package edu.caltech.android.comm;

import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import android.os.AsyncTask;
import android.util.Log;
import edu.caltech.android.picking.Pick;
import edu.caltech.android.sensor.accel.AccelSample;
import edu.caltech.csn.data.CsnMessages.HeartbeatMessage;
import edu.caltech.csn.data.CsnMessages.HeartbeatResponse;
import edu.caltech.csn.data.CsnMessages.LogMessage;
import edu.caltech.csn.data.CsnMessages.PickMessage;
import edu.caltech.csn.data.CsnMessages.PickResponse;
import edu.caltech.csn.data.CsnMessages.RecordMessage;
import edu.caltech.csn.data.CsnMessages.RegistrationMessage;
import edu.caltech.csn.data.CsnMessages.RegistrationResponse;
import edu.caltech.csn.data.CsnMessages.StatusType;

/**
 * Communicates with App Engine using Protocol Buffers.
 * 
 * @author mfaulk
 * 
 */
public class AppEngineComm implements Comm {
	// tag for logging, debugging, etc.
	private static final String TAG = "AppEngineComm";
	// used to set the debug flag in messages to GAE. If true, messages are
	// processed by GAE, but not persisted.
	private static final boolean DEBUG_MODE = false;

	private final String clientName;
	private String clientKey = null;
	// private String accountName = null;

	// URLs for communication with AppEngine
	private final String POST_ADDRESS;
	public final String REGISTER_ADDRESS;
	public final String PICK_ADDRESS;
	public final String HEARTBEAT_ADDRESS;

	private HeartbeatResponse lastHeartbeatResponse = null;
	private RegistrationResponse lastRegistrationResponse = null;

	public AppEngineComm(String url, String clientName) {
		this(url, clientName, null);
	}

	public AppEngineComm(String url, String clientName, String clientKey) {
		this.clientName = clientName;
		this.clientKey = clientKey;
		POST_ADDRESS = url;
		REGISTER_ADDRESS = POST_ADDRESS + "/pb/register";
		PICK_ADDRESS = POST_ADDRESS + "/pb/pick";
		HEARTBEAT_ADDRESS = POST_ADDRESS + "/pb/heartbeat";
	}

	@Override
	public String getClientName() {
		return clientName;
	}

	@Override
	public String getClientKey() {
		return clientKey;
	}

	@Override
	public void pick(Pick p, double lat, double lon) {
		if (clientKey == null) {
			Log.v(TAG, "client key is null. Abort pick and register.");
			register(lat, lon);
		} else {
			
			String pickDateString = createDateString(p.t);

			PickMessage pickMsg = PickMessage.newBuilder().setClient(clientKey)
					.setAmplitudeX(p.x).setAmplitudeY(p.y).setAmplitudeZ(p.z)
					.setLatitude(lat).setLongitude(lon).setDate(pickDateString)
					.setDebug(DEBUG_MODE).build();

			new PickTask().execute(pickMsg);
		}
	}

	@Override
	public void heartbeat(double lat, double lon, String parameterVersion,
			ArrayList<AccelSample> measurements) {

		if (clientKey == null) {
			Log.v(TAG, "client key is null. Abort heartbeat and register.");
			register(lat, lon);
		} else {
			String dateString = createDateString(System.currentTimeMillis());

			HeartbeatMessage.Builder hBMBuilder = HeartbeatMessage.newBuilder()
					.setClient(clientKey).setLatitude(lat).setLongitude(lon)
					.setDebug(DEBUG_MODE).setDate(dateString);

			HeartbeatMessage heartbeatMessage;
			if (measurements.isEmpty()) {
				//Log.v(TAG, "measurements is empty");
				heartbeatMessage = hBMBuilder.build();
			} else {
				LogMessage logMessage = createLogMessage(measurements, lat, lon);

				hBMBuilder.setWaveform(logMessage.toByteString()).setFormat(
						"PB");
				// format should match
				// WaveformLog.PROTOCOL_BUFFER_FORMAT_VERSION in seismocs
				// TODO: define this in the pb definition
				heartbeatMessage = hBMBuilder.build();
			}

			new HeartbeatTask().execute(heartbeatMessage);
		}
	}

	@Override
	public void heartbeat(double latitude, double longitude,
			String parameterVersion) {
		heartbeat(latitude, longitude, parameterVersion,
				new ArrayList<AccelSample>());
	}

	@Override
	public void register(double lat, double lon) {
		Log.v(TAG, "Registering...");

		RegistrationMessage regMsg = RegistrationMessage.newBuilder().setName(
				clientName).setLatitude(lat).setLongitude(lon).setSensorType(
				"ANDROIDPHONE").setMobility("phone").setDebug(DEBUG_MODE)
				.build();
		new RegistrationTask().execute(regMsg);
	}

	@Override
	public HeartbeatResponse pollHeartbeatResponse() {
		HeartbeatResponse heartbeatResp = null;
		if (lastHeartbeatResponse != null) {
			heartbeatResp = lastHeartbeatResponse;
			lastHeartbeatResponse = null;
		}
		return heartbeatResp;
	}

	@Override
	public RegistrationResponse pollRegistrationResponse() {
		RegistrationResponse regResp = null;
		if (lastRegistrationResponse != null) {
			regResp = lastRegistrationResponse;
			lastRegistrationResponse = null;
		}
		return regResp;
	}

	private class RegistrationTask extends
			AsyncTask<RegistrationMessage, Void, String> {
		protected String doInBackground(RegistrationMessage... msgs) {
			String responseString = null;

			if (msgs.length == 1) {
				RegistrationMessage msg = msgs[0];

				try {
					// send message. This should be refactored as an HTTPPost
					// method
					URL url = new URL(REGISTER_ADDRESS);
					URLConnection conn = url.openConnection();
					conn.setDoOutput(true);
					// is this correct?
					msg.writeTo(conn.getOutputStream());

					// get response
					if (!conn.getDoInput()) {
						conn.setDoInput(true);
					}
					RegistrationResponse resp = RegistrationResponse
							.parseFrom(conn.getInputStream());

					// get client ID, or handle errors
					if (resp.getStatus() == StatusType.SUCCESS) {
						Log.v("RegistrationTask", "Obtained client key: "
								+ resp.getClient());
						responseString = resp.getClient();
					} else {
						Log.v("RegistrationTask", "Status: " + resp.getStatus()
								+ " " + resp.getMessage());
					}
				} catch (Exception e) {
					Log.d("RegistrationTask", Log.getStackTraceString(e));
				}
			} else {
				Log.d("RegistrationTask", "More than one message!?!");
			}
			return responseString;
		}

		protected void onPostExecute(String result) {
			if (result == null) {
				Log.v("RegistrationTask", "received null clientKey.");
				AppEngineComm.this.clientKey = null;
				return;
			}
			if (!result.equals("authFail")) {
				Log.v("RegistrationTask", "clientKey: " + result);
				AppEngineComm.this.clientKey = result;
			} else {
				Log.v("RegistrationTask", "Authentication Failure");
			}
		}
	}

	private class HeartbeatTask extends
			AsyncTask<HeartbeatMessage, Void, HeartbeatResponse> {
		private static final String TAG = "HeartbeatTask";

		protected HeartbeatResponse doInBackground(HeartbeatMessage... msgs) {
			HeartbeatMessage msg = msgs[0];
			HeartbeatResponse resp = HeartbeatResponse.getDefaultInstance();
			try {
				// send message
				URL url = new URL(HEARTBEAT_ADDRESS);
				URLConnection conn = url.openConnection();
				conn.setDoOutput(true);
				msg.writeTo(conn.getOutputStream());

				// get response
				if (!conn.getDoInput()) {
					conn.setDoInput(true);
				}
				resp = HeartbeatResponse.parseFrom(conn.getInputStream());
			} catch (Exception e) {
				Log.v(TAG, Log.getStackTraceString(e));
				return resp;
			}

			if (resp.getStatus() == StatusType.SUCCESS) {
				//Log.v("HeartbeatTask", "Success");
			} else {
				Log.d("HeartbeatTask", "Status: " + resp.getStatus() + " "
						+ resp.getMessage());
			}
			return null;
		}

		protected void onPostExecute(HeartbeatResponse result) {
			AppEngineComm.this.lastHeartbeatResponse = result;
		}
	}

	private class PickTask extends AsyncTask<PickMessage, Void, Void> {
		/*
		 * (non-Javadoc)
		 * 
		 * @see android.os.AsyncTask#doInBackground(Params[])
		 */
		@Override
		protected Void doInBackground(PickMessage... params) {
			if (params.length != 1) {
				Log.d("PickTask", "Input " + params.length + " messages!?!");
			} else {
				PickMessage pickMsg = params[0];
				try {
					// send message. This should be refactored as an HTTPPost
					// method
					URL url = new URL(PICK_ADDRESS);
					URLConnection conn = url.openConnection();
					conn.setDoOutput(true);
					// is this correct?
					pickMsg.writeTo(conn.getOutputStream());

					// get response
					if (!conn.getDoInput()) {
						conn.setDoInput(true);
					}
					PickResponse pickResp = PickResponse.parseFrom(conn
							.getInputStream());

					if (pickResp.getStatus() == StatusType.SUCCESS) {
						Log.v("PickTask", "Successfully sent pick to " + PICK_ADDRESS);
					} else {
						Log.d("PickTask", "Status: " + pickResp.getStatus());
					}
				} catch (Exception e) {
					Log.getStackTraceString(e);
				}
			}
			return null;
		}

	}

	/**
	 * Iterate over a list of file names. Load the log data from each file, and
	 * send it as a LogMessage
	 * 
	 * possible issues: long-running AsyncTasks started from Activities can lead
	 * to trouble when the Activity is stopped / restarted (e.g. orientation
	 * change). When this happens, the AsyncTask doesn't return to the new
	 * context, and so work can be wasted. This is less of an issue when
	 * launching an AsyncTask from a Service, but for long requests, there is
	 * the likely case of the phone being turned off. Calling AsyncTask.cancel
	 * from the Service's onDestroy, and then dealing with this in the AsyncTask
	 * oncancelled() method, ought to cleanly deal with the Service lifecycle.
	 * 
	 * @author mfaulk
	 * 
	 */
//	private class LogTask extends AsyncTask<List<String>, Void, Void> {
//		private static final String TAG = "LogTask";
//		@Override
//		protected Void doInBackground(List<String>... params) {
//			Log.d(TAG,"sending logs...");
//			List<String> fileNames = params[0];
//			for (String name : fileNames) {
//				Log.d(TAG, "sending: " + name);
//				// try to load the file (old files may have been deleted since the list of names was made)
//				// if loaded, create and send a log message
//				try {
//					ArrayList<AccelSample> accelSamples = ExternalStorageManager.readLogFile(name);
//					// TODO: what should these be? Maybe save location data in the measurement database?
//					double lat = 0;
//					double lon = 0;
//					//LogMessage logMessage = createLogMessage(accelSamples, lat, lon);
//				} catch (Exception e) {
//					Log.d(TAG, Log.getStackTraceString(e));
//				}
//			}
//			return null;
//		}
//
//	}

	/**
	 * 
	 * @param measurements
	 * @return a LogMessages containing 0 or more RecordMessages.
	 */
	private LogMessage createLogMessage(ArrayList<AccelSample> measurements,
			double lat, double lon) {
		if (measurements.isEmpty()) {
			// 
			Log.v(TAG, "create a LogMessage with no RecordMessages");
			LogMessage logMessage = LogMessage.newBuilder().setStartTime(
					System.currentTimeMillis()).setLatitude(lat).setLongitude(
					lon).build();
			return logMessage;
		} else {
			// create LogMessage with RecordMessages
			AccelSample first = measurements.get(0);

			List<RecordMessage> recordMessages = new ArrayList<RecordMessage>();

			RecordMessage.Builder recordMsgBuilder = RecordMessage.newBuilder();

			long currentLogStartTime = first.t; // system milliseconds
			long recordMessageLengthMillis = 1000;

			for (AccelSample m : measurements) {
				if (m.t < currentLogStartTime + recordMessageLengthMillis) {
					// if measurement is within a second of t, add it to the
					// builder

					recordMsgBuilder.addAmplitudeX(m.x);
					recordMsgBuilder.addAmplitudeY(m.y);
					recordMsgBuilder.addAmplitudeZ(m.z);
				} else {
					// if not, build the current recordMessage, refresh the
					// builder, and then add the measurement
					RecordMessage rM = recordMsgBuilder.build();
					recordMessages.add(rM);

					recordMsgBuilder = RecordMessage.newBuilder();
				}
			}
			RecordMessage rM = recordMsgBuilder.build();
			recordMessages.add(rM);

			long startTime = first.t;
			long endTime = measurements.get(measurements.size() - 1).t;

			LogMessage logMessage = LogMessage.newBuilder().setStartTime(
					startTime).setEndTime(endTime).setLatitude(lat)
					.setLongitude(lon).addRecords(recordMessages.get(0))
					// .addAllRecords(recordMessages)
					.build();

//			Log.v(TAG, "recordsCount: " + logMessage.getRecordsCount());
//			Log.v(TAG, "latitude: " + logMessage.getLatitude());
//			Log.v(TAG, "longitude: " + logMessage.getLongitude());
//			Log.v(TAG, "serialized size: " + logMessage.getSerializedSize());
//			Log.v(TAG, "start time: " + logMessage.getStartTime());
//			Log.v(TAG, "end time: " + logMessage.getEndTime());
//			Log.v(TAG, "is initialized: " + logMessage.isInitialized());

			return logMessage;
		}

	}

	private String createDateString(long t) {
		Date date = new Date(t);

		// TODO: check the proto definition file for the date format
		SimpleDateFormat dateFormat = new SimpleDateFormat(
				"yyyy-MM-dd'T'HH:mm:ss");
		String dateString = new StringBuilder(dateFormat.format(date))
				.toString();
		return dateString;
	}

}
